﻿using Core.Entities;
using Core.Tools;
using System;
using System.Collections.Generic;

namespace Core.ObjectOperate.ReflectionImpl
{
    public sealed class ObjectOperater : IObjectOperater
    {
        private sealed class CacheValue
        {
            public readonly object Lock = new object();
            public ObjectOperater NonPublic;
            public ObjectOperater Public;

            public void AsNonPublic()
            {
                lock (Lock)
                {
                    if (NonPublic == null)
                    {
                        NonPublic = InternalAsNonPublic(Public);
                    }
                }
            }

            public void AsPublic()
            {
                lock (Lock)
                {
                    if (Public == null)
                    {
                        Public = InternalAsPublic(NonPublic);
                    }
                }
            }

            public static ObjectOperater InternalAsPublic(ObjectOperater Source)
            {
                var Result = new ObjectOperater(Source.InternalType);

                foreach (var Item in Source.Methods)
                {
                    if (Item.Value.IsPublic)
                    {
                        Result.Methods.Add(Item.Key, Item.Value);
                    }
                }

                foreach (var Item in Source.Constructors)
                {
                    if (Item.Value.IsPublic)
                    {
                        Result.Constructors.Add(Item.Key, Item.Value);
                    }
                }

                foreach (var Item in Source.Fields)
                {
                    if (Item.Value.IsPublic)
                    {
                        Result.Fields.Add(Item.Key, Item.Value);
                    }
                }

                foreach (var Item in Source.Indexes)
                {
                    var ClonedIndex = TypeHelper.Clone(Item.Value);

                    ClonedIndex.SetNonPublic(false);

                    if (ClonedIndex.CanRead || ClonedIndex.CanWrite)
                    {
                        Result.Indexes.Add(Item.Key, ClonedIndex);
                    }
                }

                foreach (var Item in Source.Properties)
                {
                    var ClonedProperty = TypeHelper.Clone(Item.Value);

                    ClonedProperty.SetNonPublic(false);

                    if (ClonedProperty.CanRead || ClonedProperty.CanWrite)
                    {
                        Result.Properties.Add(Item.Key, ClonedProperty);
                    }
                }

                return Result;
            }

            public static ObjectOperater InternalAsNonPublic(ObjectOperater Source)
            {
                var Result = new ObjectOperater(Source.InternalType, System.Reflection.BindingFlags.NonPublic);

                foreach (var Item in Source.Methods)
                {
                    Result.Methods.Add(Item.Key, Item.Value);
                }

                foreach (var Item in Source.Constructors)
                {
                    Result.Constructors.Add(Item.Key, Item.Value);
                }

                foreach (var Item in Source.Fields)
                {
                    Result.Fields.Add(Item.Key, Item.Value);
                }

                foreach (var Item in Source.Indexes)
                {
                    var ClonedIndex = TypeHelper.Clone(Item.Value);

                    ClonedIndex.SetNonPublic(true);

                    Result.Indexes.Add(Item.Key, ClonedIndex);
                }

                foreach (var Item in Source.Properties)
                {
                    var ClonedProperty = TypeHelper.Clone(Item.Value);

                    ClonedProperty.SetNonPublic(true);

                    Result.Properties.Add(Item.Key, ClonedProperty);
                }

                return Result;
            }
        }

        private static readonly Dictionary<Type, CacheValue> TypeCache = new Dictionary<Type, CacheValue>();
        private static readonly object TypeCacheLock = new object();

        public static ObjectOperater Create(Type Type, bool NonPublic = false)
        {
            CacheValue CacheItem;

            if (!TypeCache.TryGetValue(Type, out CacheItem))
            {
                lock (TypeCacheLock)
                {
                    if (!TypeCache.TryGetValue(Type, out CacheItem))
                    {
                        CacheItem = new CacheValue();

                        var Flags = System.Reflection.BindingFlags.Public;

                        if (NonPublic)
                        {
                            Flags |= System.Reflection.BindingFlags.NonPublic;
                        }

                        var ObjectOperater = new ObjectOperater(Type, Flags);

                        if (NonPublic)
                        {
                            CacheItem.NonPublic = ObjectOperater;
                        }
                        else
                        {
                            CacheItem.Public = ObjectOperater;
                        }

                        TypeCache.Add(Type, CacheItem);

                        return ObjectOperater;
                    }
                }
            }

            if (NonPublic)
            {
                if (CacheItem.NonPublic == null)
                {
                    CacheItem.AsNonPublic();
                }

                return CacheItem.NonPublic;
            }


            if (CacheItem.Public == null)
            {
                CacheItem.AsPublic();
            }

            return CacheItem.Public;
        }

        private readonly Type InternalType;

        private readonly ParamsDictionary<RuntimeMethodSign, MethodInfo> Methods;
        private readonly ParamsDictionary<RuntimeParamsSign, ConstructorInfo> Constructors;
        private readonly ParamsDictionary<RuntimeParamsSign, IndexInfo> Indexes;
        private readonly ParamsDictionary<string, PropertyInfo> Properties;
        private readonly ParamsDictionary<string, FieldInfo> Fields;
        private readonly ConstructorInfo InternalNPConstructorInfo;

        public Type Type
        {
            get
            {
                return InternalType;
            }
        }

        public int MethodsCount
        {
            get
            {
                return Methods.Count;
            }
        }

        public int IndexesCount
        {
            get
            {
                return Indexes.Count;
            }
        }

        public int PropertiesCount
        {
            get
            {
                return Properties.Count;
            }
        }

        public int FieldsCount
        {
            get
            {
                return Fields.Count;
            }
        }

        public int ConstructorsCount
        {
            get
            {
                return Constructors.Count;
            }
        }

        private ObjectOperater(Type Type, System.Reflection.BindingFlags Flags)
        {
            if (Type == null)
            {
                throw new NullReferenceException("Type");
            }

            Flags |= System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.DeclaredOnly;

            var NonPublic = (Flags & System.Reflection.BindingFlags.NonPublic) != 0;

            /* 字段 */
            Fields = new ParamsDictionary<string, FieldInfo>();

            TypeHelper.EachMembers(Type, Item => Item.GetFields(Flags), Item =>
            {
                var Field = FieldInfo.Create(Item);

                Fields[Field.Name] = Field;
            });

            /* 属性和索引器 */
            Properties = new ParamsDictionary<string, PropertyInfo>();
            Indexes = new ParamsDictionary<RuntimeParamsSign, IndexInfo>();

            TypeHelper.EachMembers(Type, Item => Item.GetProperties(Flags), Item =>
            {
                if (Item.GetIndexParameters().Length == 0)
                {
                    var Property = PropertyInfo.Create(Item, NonPublic);

                    Properties[Property.Name] = Property;
                }
                else
                {
                    var Index = IndexInfo.Create(Item, NonPublic);

                    Indexes[new RuntimeParamsSign(Index.ParamsTypes)] = Index;
                }
            });


            /* 构造函数 */
            Constructors = new ParamsDictionary<RuntimeParamsSign, ConstructorInfo>();

            foreach (var Item in Type.GetConstructors(Flags))
            {
                var Constructor = ConstructorInfo.Create(Item);

                if (Constructor.ParamsTypes.Length == 0)
                {
                    InternalNPConstructorInfo = Constructor;
                }

                Constructors[new RuntimeParamsSign(Constructor.ParamsTypes)] = Constructor;
            }


            /* 函数 */
            Methods = new ParamsDictionary<RuntimeMethodSign, MethodInfo>();

            foreach (var Item in TypeHelper.GetMethodsIsNotGenericAndRemovedDuplicateSignaturesAndProperties(Type, Flags))
            {
                var Method = MethodInfo.Create(Item);

                Methods[new RuntimeMethodSign(Method.Name, Method.ParamsTypes)] = Method;
            }

            InternalType = Type;
        }

        private ObjectOperater(Type Type)
        {
            InternalType = Type;

            Methods = new ParamsDictionary<RuntimeMethodSign, MethodInfo>();
            Constructors = new ParamsDictionary<RuntimeParamsSign, ConstructorInfo>();
            Indexes = new ParamsDictionary<RuntimeParamsSign, IndexInfo>();
            Properties = new ParamsDictionary<string, PropertyInfo>();
            Fields = new ParamsDictionary<string, FieldInfo>();
        }

        public object InvokeMethod(object Object, string MethodName, object[] Params)
        {
            if (MethodName == null)
            {
                throw new NullReferenceException("MethodName");
            }

            if (Params == null)
            {
                throw new NullReferenceException("Params");
            }

            RuntimeMethodSign MethodSign = new RuntimeMethodSign(MethodName, Params);

            MethodInfo Method;

            if (Methods.TryGetValue(MethodSign, out Method))
            {
                return Method.Invoke(Object, Params);
            }

            throw new ArgumentException("Method not found.");
        }

        public object InvokeMethod(object Object, int MethodIndex, object[] Params)
        {
            if (MethodIndex < 0 || MethodIndex >= MethodsCount)
            {
                throw new IndexOutOfRangeException("MethodIndex");
            }

            if (Params == null)
            {
                throw new NullReferenceException("Params");
            }

            return Methods[MethodIndex].Value.Invoke(Object, Params);
        }

        public int GetMethodIndex(string MethodName, Type[] MethodTypes)
        {
            if (MethodName == null)
            {
                throw new NullReferenceException("MethodName");
            }

            if (MethodTypes == null)
            {
                throw new NullReferenceException("MethodTypes");
            }

            RuntimeMethodSign MethodSign = new RuntimeMethodSign(MethodName, MethodTypes);

            return Methods.FindLastIndex(MethodSign);
        }

        public string GetMethodName(int MethodIndex)
        {
            if (MethodIndex < 0 || MethodIndex >= MethodsCount)
            {
                throw new IndexOutOfRangeException("MethodIndex");
            }

            return Methods[MethodIndex].Value.Name;
        }

        public Type[] GetMethodTypes(int MethodIndex)
        {
            if (MethodIndex < 0 || MethodIndex >= MethodsCount)
            {
                throw new IndexOutOfRangeException("MethodIndex");
            }

            return Methods[MethodIndex].Value.ParamsTypes;
        }

        public Type GetMethodType(int MethodIndex)
        {
            if (MethodIndex < 0 || MethodIndex >= MethodsCount)
            {
                throw new IndexOutOfRangeException("MethodIndex");
            }

            return Methods[MethodIndex].Value.ReturnType;
        }

        public Type GetMethodType(string MethodName, Type[] MethodTypes)
        {
            if (MethodName == null)
            {
                throw new NullReferenceException("MethodName");
            }

            if (MethodTypes == null)
            {
                throw new NullReferenceException("MethodTypes");
            }

            RuntimeMethodSign MethodSign = new RuntimeMethodSign(MethodName, MethodTypes);

            MethodInfo Method;

            if (Methods.TryGetValue(MethodSign, out Method))
            {
                return Method.ReturnType;
            }

            throw new ArgumentException("Method not found.");
        }

        public object GetIndexValue(object Object, object[] Params)
        {
            if (Params == null)
            {
                throw new NullReferenceException("Params");
            }

            RuntimeParamsSign ParamsSign = new RuntimeParamsSign(Params);

            IndexInfo Index;

            if (Indexes.TryGetValue(ParamsSign, out Index))
            {
                return Index.GetValue(Object, Params);
            }

            throw new ArgumentException("Indexer not found.");
        }

        public void SetIndexValue(object Object, object[] Params, object Value)
        {
            if (Params == null)
            {
                throw new NullReferenceException("Params");
            }

            RuntimeParamsSign ParamsSign = new RuntimeParamsSign(Params);

            IndexInfo Index;

            if (Indexes.TryGetValue(ParamsSign, out Index))
            {
                Index.SetValue(Object, Params, Value);

                return;
            }

            throw new ArgumentException("Indexer not found.");
        }

        public object GetIndexValue(object Object, int IndexIndex, object[] Params)
        {
            if (IndexIndex < 0 || IndexIndex >= IndexesCount)
            {
                throw new IndexOutOfRangeException("IndexIndex");
            }

            if (Params == null)
            {
                throw new NullReferenceException("Params");
            }

            return Indexes[IndexIndex].Value.GetValue(Object, Params);
        }

        public void SetIndexValue(object Object, int IndexIndex, object[] Params, object Value)
        {
            if (IndexIndex < 0 || IndexIndex >= IndexesCount)
            {
                throw new IndexOutOfRangeException("IndexIndex");
            }

            if (Params == null)
            {
                throw new NullReferenceException("Params");
            }

            Indexes[IndexIndex].Value.SetValue(Object, Params, Value);
        }

        public int GetIndexIndex(Type[] IndexTypes)
        {
            if (IndexTypes == null)
            {
                throw new NullReferenceException("IndexTypes");
            }

            RuntimeParamsSign ParamsSign = new RuntimeParamsSign(IndexTypes);

            return Indexes.FindLastIndex(ParamsSign);
        }

        public Type[] GetIndexTypes(int IndexIndex)
        {
            if (IndexIndex < 0 || IndexIndex >= IndexesCount)
            {
                throw new IndexOutOfRangeException("IndexIndex");
            }

            return Indexes[IndexIndex].Value.ParamsTypes;
        }

        public Type GetIndexType(int IndexIndex)
        {
            if (IndexIndex < 0 || IndexIndex >= IndexesCount)
            {
                throw new IndexOutOfRangeException("IndexIndex");
            }

            return Indexes[IndexIndex].Value.Type;
        }

        public Type GetIndexType(Type[] IndexTypes)
        {
            if (IndexTypes == null)
            {
                throw new NullReferenceException("IndexTypes");
            }

            RuntimeParamsSign ParamsSign = new RuntimeParamsSign(IndexTypes);

            IndexInfo Index;

            if (Indexes.TryGetValue(ParamsSign, out Index))
            {
                return Index.Type;
            }

            throw new ArgumentException("Indexer not found.");
        }

        public object GetPropertyValue(object Object, string PropertyName)
        {
            if (PropertyName == null)
            {
                throw new NullReferenceException("PropertyName");
            }

            PropertyInfo Property;

            if (Properties.TryGetValue(PropertyName, out Property))
            {
                return Property.GetValue(Object);
            }

            throw new ArgumentException("Property not found.");
        }

        public void SetPropertyValue(object Object, string PropertyName, object Value)
        {
            if (PropertyName == null)
            {
                throw new NullReferenceException("PropertyName");
            }

            PropertyInfo Property;

            if (Properties.TryGetValue(PropertyName, out Property))
            {
                Property.SetValue(Object, Value);

                return;
            }

            throw new ArgumentException("Property not found.");
        }

        public object GetPropertyValue(object Object, int PropertyIndex)
        {
            if (PropertyIndex < 0 || PropertyIndex >= PropertiesCount)
            {
                throw new IndexOutOfRangeException("PropertyIndex");
            }

            return Properties[PropertyIndex].Value.GetValue(Object);
        }

        public void SetPropertyValue(object Object, int PropertyIndex, object Value)
        {
            if (PropertyIndex < 0 || PropertyIndex >= PropertiesCount)
            {
                throw new IndexOutOfRangeException("PropertyIndex");
            }

            Properties[PropertyIndex].Value.SetValue(Object, Value);
        }

        public int GetPropertyIndex(string PropertyName)
        {
            if (PropertyName == null)
            {
                throw new NullReferenceException("PropertyName");
            }

            return Properties.FindLastIndex(PropertyName);
        }

        public string GetPropertyName(int PropertyIndex)
        {
            if (PropertyIndex < 0 || PropertyIndex >= PropertiesCount)
            {
                throw new IndexOutOfRangeException("PropertyIndex");
            }

            return Properties[PropertyIndex].Value.Name;
        }

        public Type GetPropertyType(int PropertyIndex)
        {
            if (PropertyIndex < 0 || PropertyIndex >= PropertiesCount)
            {
                throw new IndexOutOfRangeException("PropertyIndex");
            }

            return Properties[PropertyIndex].Value.Type;
        }

        public Type GetPropertyType(string PropertyName)
        {
            if (PropertyName == null)
            {
                throw new NullReferenceException("PropertyName");
            }

            PropertyInfo Property;

            if (Properties.TryGetValue(PropertyName, out Property))
            {
                return Property.Type;
            }

            throw new ArgumentException("Property not found.");
        }

        public object GetFieldValue(object Object, string FieldName)
        {
            if (FieldName == null)
            {
                throw new NullReferenceException("FieldName");
            }

            FieldInfo Field;

            if (Fields.TryGetValue(FieldName, out Field))
            {
                return Field.GetValue(Object);
            }

            throw new ArgumentException("Field not found.");
        }

        public void SetFieldValue(object Object, string FieldName, object Value)
        {
            if (FieldName == null)
            {
                throw new NullReferenceException("FieldName");
            }

            FieldInfo Field;

            if (Fields.TryGetValue(FieldName, out Field))
            {
                Field.SetValue(Object, Value);

                return;
            }

            throw new ArgumentException("Field not found.");
        }

        public object GetFieldValue(object Object, int FieldIndex)
        {
            if (FieldIndex < 0 || FieldIndex >= FieldsCount)
            {
                throw new IndexOutOfRangeException("FieldIndex");
            }

            return Fields[FieldIndex].Value.GetValue(Object);
        }

        public void SetFieldValue(object Object, int FieldIndex, object Value)
        {
            if (FieldIndex < 0 || FieldIndex >= FieldsCount)
            {
                throw new IndexOutOfRangeException("FieldIndex");
            }

            Fields[FieldIndex].Value.SetValue(Object, Value);
        }

        public int GetFieldIndex(string FieldName)
        {
            if (FieldName == null)
            {
                throw new NullReferenceException("FieldName");
            }

            return Fields.FindLastIndex(FieldName);
        }

        public string GetFieldName(int FieldIndex)
        {
            if (FieldIndex < 0 || FieldIndex >= FieldsCount)
            {
                throw new IndexOutOfRangeException("FieldIndex");
            }

            return Fields[FieldIndex].Value.Name;
        }

        public Type GetFieldType(int FieldIndex)
        {
            if (FieldIndex < 0 || FieldIndex >= FieldsCount)
            {
                throw new IndexOutOfRangeException("FieldIndex");
            }

            return Fields[FieldIndex].Value.Type;
        }

        public Type GetFieldType(string FieldName)
        {
            if (FieldName == null)
            {
                throw new NullReferenceException("FieldName");
            }

            FieldInfo Field;

            if (Fields.TryGetValue(FieldName, out Field))
            {
                return Field.Type;
            }

            throw new ArgumentException("Field not found.");
        }

        public object New(object[] Params)
        {
            if (Params == null)
            {
                throw new NullReferenceException("Params");
            }

            RuntimeParamsSign ParamsSign = new RuntimeParamsSign(Params);

            ConstructorInfo Constructor;

            if (Constructors.TryGetValue(ParamsSign, out Constructor))
            {
                return Constructor.New(Params);
            }

            throw new ArgumentException("Constructor not found.");
        }

        public object New()
        {
            if (InternalNPConstructorInfo == null)
            {
                throw new ArgumentException("No parameterless constructor.");
            }

            return InternalNPConstructorInfo.New(null);
        }

        public int GetConstructorIndex(Type[] ConstructorsTypes)
        {
            if (ConstructorsTypes == null)
            {
                throw new NullReferenceException("ConstructorsTypes");
            }

            RuntimeParamsSign ParamsSign = new RuntimeParamsSign(ConstructorsTypes);

            return Constructors.FindLastIndex(ParamsSign);
        }

        public Type[] GetConstructorTypes(int ConstructorIndex)
        {
            if (ConstructorIndex < 0 || ConstructorIndex >= ConstructorsCount)
            {
                throw new IndexOutOfRangeException("ConstructorIndex");
            }

            return Constructors[ConstructorIndex].Value.ParamsTypes;
        }
    }
}